// For license of this file, see <project-root-folder>/LICENSE.md.

#include "services/standard/standardfeed.h"

#include "core/feedsmodel.h"
#include "definitions/definitions.h"
#include "gui/feedmessageviewer.h"
#include "gui/feedsview.h"
#include "miscellaneous/databasequeries.h"
#include "miscellaneous/iconfactory.h"
#include "miscellaneous/settings.h"
#include "miscellaneous/simplecrypt/simplecrypt.h"
#include "miscellaneous/textfactory.h"
#include "network-web/networkfactory.h"
#include "services/abstract/recyclebin.h"
#include "services/standard/atomparser.h"
#include "services/standard/gui/formstandardfeeddetails.h"
#include "services/standard/rdfparser.h"
#include "services/standard/rssparser.h"
#include "services/standard/standardserviceroot.h"

#include <QDomDocument>
#include <QDomElement>
#include <QDomNode>
#include <QPointer>
#include <QTextCodec>
#include <QVariant>
#include <QXmlStreamReader>

StandardFeed::StandardFeed(RootItem* parent_item)
  : Feed(parent_item) {
  m_passwordProtected = false;
  m_username = QString();
  m_password = QString();
  m_networkError = QNetworkReply::NoError;
  m_type = Rss0X;
  m_encoding = QString();
}

StandardFeed::StandardFeed(const StandardFeed& other)
  : Feed(other) {
  m_passwordProtected = other.passwordProtected();
  m_username = other.username();
  m_password = other.password();
  m_networkError = other.networkError();
  m_type = other.type();
  m_encoding = other.encoding();
}

StandardFeed::~StandardFeed() {
  qDebug("Destroying Feed instance.");
}

QList<QAction*> StandardFeed::contextMenu() {
  return serviceRoot()->getContextMenuForFeed(this);
}

QString StandardFeed::additionalTooltip() const {
  return Feed::additionalTooltip() + tr("\nNetwork status: %1\n"
                                        "Encoding: %2\n"
                                        "Type: %3").arg(NetworkFactory::networkErrorText(m_networkError),
                                                        encoding(),
                                                        StandardFeed::typeToString(type()));
}

bool StandardFeed::canBeEdited() const {
  return true;
}

bool StandardFeed::canBeDeleted() const {
  return true;
}

StandardServiceRoot* StandardFeed::serviceRoot() const {
  return qobject_cast<StandardServiceRoot*>(getParentServiceRoot());
}

bool StandardFeed::editViaGui() {
  QScopedPointer<FormStandardFeedDetails> form_pointer(new FormStandardFeedDetails(serviceRoot(), qApp->mainFormWidget()));
  form_pointer.data()->addEditFeed(this, nullptr);
  return false;
}

bool StandardFeed::deleteViaGui() {
  if (removeItself()) {
    serviceRoot()->requestItemRemoval(this);
    return true;
  }
  else {
    return false;
  }
}

QString StandardFeed::typeToString(StandardFeed::Type type) {
  switch (type) {
    case Atom10:
      return QSL("ATOM 1.0");

    case Rdf:
      return QSL("RDF (RSS 1.0)");

    case Rss0X:
      return QSL("RSS 0.91/0.92/0.93");

    case Rss2X:
    default:
      return QSL("RSS 2.0/2.0.1");
  }
}

void StandardFeed::fetchMetadataForItself() {
  QPair<StandardFeed*, QNetworkReply::NetworkError> metadata = guessFeed(url(), username(), password());

  if (metadata.first != nullptr && metadata.second == QNetworkReply::NoError) {
    // Some properties are not updated when new metadata are fetched.
    metadata.first->setParent(parent());
    metadata.first->setUrl(url());
    metadata.first->setPasswordProtected(passwordProtected());
    metadata.first->setUsername(username());
    metadata.first->setPassword(password());
    metadata.first->setAutoUpdateType(autoUpdateType());
    metadata.first->setAutoUpdateInitialInterval(autoUpdateInitialInterval());
    editItself(metadata.first);
    delete metadata.first;

    // Notify the model about fact, that it needs to reload new information about
    // this item, particularly the icon.
    serviceRoot()->itemChanged(QList<RootItem*>() << this);
  }
  else {
    qApp->showGuiMessage(tr("Metadata not fetched"),
                         tr("Metadata was not fetched because: %1.").arg(NetworkFactory::networkErrorText(metadata.second)),
                         QSystemTrayIcon::Critical);
  }
}

QPair<StandardFeed*, QNetworkReply::NetworkError> StandardFeed::guessFeed(const QString& url,
                                                                          const QString& username,
                                                                          const QString& password) {
  QPair<StandardFeed*, QNetworkReply::NetworkError> result;
  result.first = nullptr;
  QByteArray feed_contents;

  QList<QPair<QByteArray, QByteArray>> headers;
  headers << NetworkFactory::generateBasicAuthHeader(username, password);

  NetworkResult network_result = NetworkFactory::performNetworkOperation(url,
                                                                         qApp->settings()->value(GROUP(Feeds),
                                                                                                 SETTING(Feeds::UpdateTimeout)).toInt(),
                                                                         QByteArray(),
                                                                         feed_contents,
                                                                         QNetworkAccessManager::GetOperation,
                                                                         headers);

  result.second = network_result.first;

  if (result.second == QNetworkReply::NoError || !feed_contents.isEmpty()) {
    if (result.first == nullptr) {
      result.first = new StandardFeed();
    }

    // Feed XML was obtained, now we need to try to guess
    // its encoding before we can read further data.
    QString xml_schema_encoding;
    QString xml_contents_encoded;
    QString enc = QRegularExpression(QSL("encoding=\"([A-Z0-9\\-]+)\""),
                                     QRegularExpression::PatternOption::CaseInsensitiveOption).match(feed_contents).captured(1);

    if (!enc.isEmpty()) {
      // Some "encoding" attribute was found get the encoding
      // out of it.
      xml_schema_encoding = enc;
    }

    QTextCodec* custom_codec = QTextCodec::codecForName(xml_schema_encoding.toLocal8Bit());

    if (custom_codec != nullptr) {
      // Feed encoding was probably guessed.
      xml_contents_encoded = custom_codec->toUnicode(feed_contents);
      result.first->setEncoding(xml_schema_encoding);
    }
    else {
      // Feed encoding probably not guessed, set it as
      // default.
      xml_contents_encoded = feed_contents;
      result.first->setEncoding(DEFAULT_FEED_ENCODING);
    }

    // Feed XML was obtained, guess it now.
    QDomDocument xml_document;
    QString error_msg;
    int error_line, error_column;

    if (!xml_document.setContent(xml_contents_encoded,
                                 &error_msg,
                                 &error_line,
                                 &error_column)) {
      qDebug("XML of feed '%s' is not valid and cannot be loaded. Error: '%s' "
             "(line %d, column %d).",
             qPrintable(url),
             qPrintable(error_msg),
             error_line, error_column);
      result.second = QNetworkReply::UnknownContentError;

      // XML is invalid, exit.
      return result;
    }

    QDomElement root_element = xml_document.documentElement();
    QString root_tag_name = root_element.tagName();

    QList<QString> icon_possible_locations;
    icon_possible_locations.append(url);

    if (root_tag_name == QL1S("rdf:RDF")) {
      // We found RDF feed.
      QDomElement channel_element = root_element.namedItem(QSL("channel")).toElement();

      result.first->setType(Rdf);
      result.first->setTitle(channel_element.namedItem(QSL("title")).toElement().text());
      result.first->setDescription(channel_element.namedItem(QSL("description")).toElement().text());
      QString source_link = channel_element.namedItem(QSL("link")).toElement().text();

      if (!source_link.isEmpty()) {
        icon_possible_locations.prepend(source_link);
      }
    }
    else if (root_tag_name == QL1S("rss")) {
      // We found RSS 0.91/0.92/0.93/2.0/2.0.1 feed.
      QString rss_type = root_element.attribute("version", "2.0");

      if (rss_type == QL1S("0.91") || rss_type == QL1S("0.92") || rss_type == QL1S("0.93")) {
        result.first->setType(Rss0X);
      }
      else {
        result.first->setType(Rss2X);
      }

      QDomElement channel_element = root_element.namedItem(QSL("channel")).toElement();

      result.first->setTitle(channel_element.namedItem(QSL("title")).toElement().text());
      result.first->setDescription(channel_element.namedItem(QSL("description")).toElement().text());
      QString source_link = channel_element.namedItem(QSL("link")).toElement().text();

      if (!source_link.isEmpty()) {
        icon_possible_locations.prepend(source_link);
      }
    }
    else if (root_tag_name == QL1S("feed")) {
      // We found ATOM feed.
      result.first->setType(Atom10);
      result.first->setTitle(root_element.namedItem(QSL("title")).toElement().text());
      result.first->setDescription(root_element.namedItem(QSL("subtitle")).toElement().text());
      QString source_link = root_element.namedItem(QSL("link")).toElement().text();

      if (!source_link.isEmpty()) {
        icon_possible_locations.prepend(source_link);
      }
    }
    else {
      // File was downloaded and it really was XML file
      // but feed format was NOT recognized.
      result.second = QNetworkReply::UnknownContentError;
    }

    // Try to obtain icon.
    QIcon icon_data;

    if ((result.second = NetworkFactory::downloadIcon(icon_possible_locations,
                                                      DOWNLOAD_TIMEOUT,
                                                      icon_data)) == QNetworkReply::NoError) {
      // Icon for feed was downloaded and is stored now in _icon_data.
      result.first->setIcon(icon_data);
    }
  }

  return result;
}

Qt::ItemFlags StandardFeed::additionalFlags() const {
  return Qt::ItemIsDragEnabled;
}

bool StandardFeed::performDragDropChange(RootItem* target_item) {
  auto* feed_new = new StandardFeed(*this);

  feed_new->setParent(target_item);

  if (editItself(feed_new)) {
    serviceRoot()->requestItemReassignment(this, target_item);
    delete feed_new;
    return true;
  }
  else {
    delete feed_new;
    return false;
  }
}

bool StandardFeed::removeItself() {
  QSqlDatabase database = qApp->database()->connection(metaObject()->className());

  return DatabaseQueries::deleteFeed(database, customId().toInt(), getParentServiceRoot()->accountId());
}

bool StandardFeed::addItself(RootItem* parent) {
  // Now, add feed to persistent storage.
  QSqlDatabase database = qApp->database()->connection(metaObject()->className());
  bool ok;
  int new_id = DatabaseQueries::addFeed(database, parent->id(), parent->getParentServiceRoot()->accountId(), title(),
                                        description(), creationDate(), icon(), encoding(), url(), passwordProtected(),
                                        username(), password(), autoUpdateType(), autoUpdateInitialInterval(), type(), &ok);

  if (!ok) {
    // Query failed.
    return false;
  }
  else {
    // New feed was added, fetch is primary id from the database.
    setId(new_id);
    setCustomId(QString::number(new_id));
    return true;
  }
}

bool StandardFeed::editItself(StandardFeed* new_feed_data) {
  QSqlDatabase database = qApp->database()->connection(metaObject()->className());
  StandardFeed* original_feed = this;
  RootItem* new_parent = new_feed_data->parent();

  if (!DatabaseQueries::editFeed(database, new_parent->id(), original_feed->id(), new_feed_data->title(),
                                 new_feed_data->description(), new_feed_data->icon(),
                                 new_feed_data->encoding(), new_feed_data->url(), new_feed_data->passwordProtected(),
                                 new_feed_data->username(), new_feed_data->password(),
                                 new_feed_data->autoUpdateType(), new_feed_data->autoUpdateInitialInterval(),
                                 new_feed_data->type())) {
    // Persistent storage update failed, no way to continue now.
    return false;
  }

  // Setup new model data for the original item.
  original_feed->setTitle(new_feed_data->title());
  original_feed->setDescription(new_feed_data->description());
  original_feed->setIcon(new_feed_data->icon());
  original_feed->setEncoding(new_feed_data->encoding());
  original_feed->setDescription(new_feed_data->description());
  original_feed->setUrl(new_feed_data->url());
  original_feed->setPasswordProtected(new_feed_data->passwordProtected());
  original_feed->setUsername(new_feed_data->username());
  original_feed->setPassword(new_feed_data->password());
  original_feed->setAutoUpdateType(new_feed_data->autoUpdateType());
  original_feed->setAutoUpdateInitialInterval(new_feed_data->autoUpdateInitialInterval());
  original_feed->setType(new_feed_data->type());

  // Editing is done.
  return true;
}

StandardFeed::Type StandardFeed::type() const {
  return m_type;
}

void StandardFeed::setType(StandardFeed::Type type) {
  m_type = type;
}

bool StandardFeed::passwordProtected() const {
  return m_passwordProtected;
}

void StandardFeed::setPasswordProtected(bool passwordProtected) {
  m_passwordProtected = passwordProtected;
}

QString StandardFeed::username() const {
  return m_username;
}

void StandardFeed::setUsername(const QString& username) {
  m_username = username;
}

QString StandardFeed::password() const {
  return m_password;
}

void StandardFeed::setPassword(const QString& password) {
  m_password = password;
}

QString StandardFeed::encoding() const {
  return m_encoding;
}

void StandardFeed::setEncoding(const QString& encoding) {
  m_encoding = encoding;
}

QList<Message> StandardFeed::obtainNewMessages(bool* error_during_obtaining) {
  QByteArray feed_contents;
  int download_timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();

  QList<QPair<QByteArray, QByteArray>> headers;
  headers << NetworkFactory::generateBasicAuthHeader(username(), password());

  m_networkError = NetworkFactory::performNetworkOperation(url(),
                                                           download_timeout,
                                                           QByteArray(),
                                                           feed_contents,
                                                           QNetworkAccessManager::GetOperation,
                                                           headers).first;

  if (m_networkError != QNetworkReply::NoError) {
    qWarning("Error during fetching of new messages for feed '%s' (id %d).", qPrintable(url()), id());
    setStatus(NetworkError);
    *error_during_obtaining = true;
    return QList<Message>();
  }
  else {
    *error_during_obtaining = false;
  }

  // Encode downloaded data for further parsing.
  QTextCodec* codec = QTextCodec::codecForName(encoding().toLocal8Bit());
  QString formatted_feed_contents;

  if (codec == nullptr) {
    // No suitable codec for this encoding was found.
    // Use non-converted data.
    formatted_feed_contents = feed_contents;
  }
  else {
    formatted_feed_contents = codec->toUnicode(feed_contents);
  }

  // Feed data are downloaded and encoded.
  // Parse data and obtain messages.
  QList<Message> messages;

  switch (type()) {
    case StandardFeed::Rss0X:
    case StandardFeed::Rss2X:
      messages = RssParser(formatted_feed_contents).messages();
      break;

    case StandardFeed::Rdf:
      messages = RdfParser().parseXmlData(formatted_feed_contents);
      break;

    case StandardFeed::Atom10:
      messages = AtomParser(formatted_feed_contents).messages();

    default:
      break;
  }

  return messages;
}

QNetworkReply::NetworkError StandardFeed::networkError() const {
  return m_networkError;
}

StandardFeed::StandardFeed(const QSqlRecord& record) : Feed(record) {
  setEncoding(record.value(FDS_DB_ENCODING_INDEX).toString());
  setPasswordProtected(record.value(FDS_DB_PROTECTED_INDEX).toBool());
  setUsername(record.value(FDS_DB_USERNAME_INDEX).toString());

  if (record.value(FDS_DB_PASSWORD_INDEX).toString().isEmpty()) {
    setPassword(record.value(FDS_DB_PASSWORD_INDEX).toString());
  }
  else {
    setPassword(TextFactory::decrypt(record.value(FDS_DB_PASSWORD_INDEX).toString()));
  }

  setAutoUpdateType(static_cast<Feed::AutoUpdateType>(record.value(FDS_DB_UPDATE_TYPE_INDEX).toInt()));
  setAutoUpdateInitialInterval(record.value(FDS_DB_UPDATE_INTERVAL_INDEX).toInt());
  m_networkError = QNetworkReply::NoError;
}
